简介
libfuzzer是进程内的,覆盖率指导的,进化的fuzzing引擎。
就是变异,覆盖率那些都给你做好了,你只需要定义LLVMFuzzerTestOneInput
,将编译的数据喂给要fuzz的目标函数就行
libfuzzer还在不断开发完善中,所以可能需要当前版本的clang或者没那么就版本的clang进行编译
注意:clang6.0开始就默认在里面包含了libfuzzer
使用——需要实现一个fuzz target
所谓fuzz target,就是去实现LLVMFuzzerTestOneInput
,下面是文档给出的示例,就是实现一个函数,将Data这个字节数组,传递给你要测试的API。
1 | // fuzz_target.cc |
有以下注意事项:
1、fuzzing引擎会在同一个进程用不同的输入执行LLVMFuzzerTestOneInput
很多次
2、LLVMFuzzerTestOneInput
比如容忍任何形式的输入(空,很长的数据,格式错误的数据等)
3、任何输入都不能调用exit退出(毕竟还要继续fuzz啊)
4、可以使用线程,应该线程也应在函数结束前结束,不知道对不对,后面是原文(It may use threads but ideally all threads should be joined at the end of the function.)
5、必须具有确定性,因为不确定性降低fuzz的效率(比如不会根据输入去随机选择路径)
6、必须快,避免立方以上的复杂性,进行日志记录或者过多的内存消耗
7、理想的情况下,不要修改任何的全局状态
8、通常目标约窄越好,比如目标可以解析多种数据格式,就写成多个目标,每个格式一个
使用libfuzzer
Clang6.0开始包含了libfuzzer,
使用就是在编译时使用-fsanitize=fuzzer
即可,当然也可以加上 AddressSanitizer (ASAN),UndefinedBehaviorSanitizer (UBSAN),以及MemorySanitizer (MSAN),下面是例子
1 | clang -g -O1 -fsanitize=fuzzer mytarget.c # Builds the fuzz target w/o sanitizers |
当然也可以单独编译libfuzzer,之后链接起来就可以了(这是以前文档的做法了,当然假如你想使用最新的libfuzzer也就只能像上面那样了)
1 | clang -fsanitize-coverage=trace-pc-guard -fsanitize=address your_lib.cc fuzz_target.cc libFuzzer.a -o my_fuzzer |
语料库
libfuzzer可以在没有任何初始种子的情况下工作,但是如果被测库接受复杂的结构化输入,效率将会降低。
所以对于一些结构化的输入最好提供语料库(原始样本),这样可以提高效率
如果语料库很大,可以先将其最小化,同事保留完成的覆盖率,使用-merge=1即可
1 | mkdir NEW_CORPUS_DIR # Store minimized corpus here. |
假如想将其他的语料库加到现在的,只讲有新覆盖率的加入
1 | ./my_fuzzer -merge=1 CURRENT_CORPUS_DIR NEW_POTENTIALLY_INTERESTING_INPUTS_DIR |
运行
最好创建一个目录,里面包含初始的“种子”样本输入
1 | mkdir CORPUS_DIR |
之后运行就行,当然可以加一些参数
1 | ./my_fuzzer CORPUS_DIR # -max_len=1000 -jobs=20 ... |
一旦fuzzer发现了有趣的testcase,就是能够触发新的路径的testcase,就会添加到CORPUS_DIR
默认情况,fuzzer会持续运行,直到发现一个bug,任何的crash或者sanitizer的报错,都会停止fuzzing,之后保存触发crash的样本会默认会保存到当前目录,通常命名为crash-<sha1>, leak-<sha1>, 或者 timeout-<sha1>
并行fuzz
每个libfuzzer进程都是单线程,除非这个库自己起了多线程。
但是可以多个libfuzzer共享一个语料库目录,那么一个模糊器进程找到的任何新输入将对其他模糊器进程可用(除非你是用-reload=0关闭了这个reload预料库的功能)
我们可以通过-jobs=N
来控制,N个fuzzing jobs必须完成它的使命(就是找到bug或者time/iteration达到我们限制的上限了),jobs是在worker进程中运行的,默认使用一半可用的cpu核心;当然我们可以用参数-workers=N
指定worker的个数。比如,在一个12核的机器使用-jobs=30
,就默认运行6个worker,最好的情况每个worker可能5个bug
fork模式
这还是一个实验的模式,-fork=N
表示并行的jobs的数量,这个应该跟上面的jobs不是同一个
这个模式在每个process中开启了oom-, timeout-, and crash-resistant
最上层的libfuzzer不会做任何fuzzing,会产生最多N个并发的子进程,并为子进程提供corpus的小的随机子集。子进程退出后,最上层的libfuzzer会将其产生的corpus合并到主corpus
下面是相关的一些flags:
1 | -ignore_ooms |
这个其实是想最终将-jobs = N
和-workers = N
替换为-fork = N
。
merge恢复
有时候合并较大的语料库,比较耗时,kill掉之后也可以恢复,需要使用-merge_control_file
(指定用于合并过程的控制文件)
推荐使用killall -SIGUSR1 /path/to/fuzzer/binary
来优雅地kill
下面是官方例子
1 | % rm -f SomeLocalPath |
运行选项
可以传递零个或多个语料库目录作为命令行参数。模糊器将从这些语料库目录中的每一个中读取测试输入,并且所生成的任何新测试输入将被写回到第一个语料库目录中。
1 | ./fuzzer [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ] |
如果给的是文件列表不是目录,那就是相当于回归测试,漏洞复现了
文档中已经对-flag列得比较详细了,我就不复制了,你用-help=1得到的信息启示更详细
但是列一些比较有趣的/常用的
1 | -max_len:最大长度 |
输出解读
文档说信息是输出到stderr,比如下面的例子
1 | INFO: Seed: 1523017872 |
首先是输出有关fuzzer的选项和配置的信息,包括当前的初始种子,当然你可以通过-seed=N
来指定
接下来就是输出事件,还有统计信息,
事件有下面的
1 | READ:fuzzer已经从语料库目录读取了所有输入样本 |
每一行的统计信息:
1 | cov:当前语料库所衣服挂的代码块或者边数 |
对于NEW和REDUCE事件,还有有关产生新输入的变异操作的信息:
1 | L:新输入的大小 |
例子
1 | cat << EOF > test_fuzzer.cc |
更多例子可以在这里看到:
https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md
进阶功能
字典
使用-dict=DICTIONARY_FILE
指定,这样应该可以快速绕过magic values
使用字典可能会大大提高搜索速度。 字典语法类似于AFL的-x选项所使用的语法。
1 | # Lines starting with '#' and empty lines are ignored. |
Tracing CMP instructions
跟踪CMP指令
编译的时候加上:-fsanitize-coverage=trace-cmp
libFuzzer将拦截CMP指令并根据截获的CMP指令的参数去引导突变。 这可能减缓模糊的速度,但是很有可能会改善结果。
Value Profile
必须开启上面的编译标志,运行的时候加上-use_value_profile=1
,之后fuzzer就会收集比较指令的参数的value profiles,并将一些新的values作为新的覆盖。
实现的操作如下:
1、编译器通过对所有CMP指令的插桩,获取两个参数
2、获取两个参数后计算 (caller_pc&4095) | (popcnt(Arg1 ^ Arg2) << 12),使用这个值,在bitset中设置一个bit
3、在 bitset 中设置了新的bit说明找到新路径了
此功能可能会发现许多有趣的输入,但是有两个缺点。 首先,可能会导致速度降低2倍。 其次,语料库可能增长数倍。
Fuzzer-friendly build mode
1、一些软件使用伪随机数生成器,导致可能同一个输入,但是导致路径不一样
2、或者png会有校验和
我们可以用FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
这个宏来使代码更加友好,下面是例子
1 | void MyInitPRNG() { |
与afl一起fuzz
libfuzzer可以使用afl的发现的语料库,比如下面的例子
1 | ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@ |
当然afl也可以fuzz,LLVMFuzzerTestOneInput写的目标,具体可以看这:https://github.com/llvm/llvm-project/tree/master/compiler-rt/lib/fuzzer/afl
我的fuzzer是否优秀?
这个可以通过查看代码覆盖率来衡量,看看我们的语料库或者LLVMFuzzerTestOneInput函数是否有改进的空间
https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
具体例子可以看这个
https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md#visualizing-coverage
自定义的变异
我们可以自定义变异,可以参考: https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md
启动初始化
如果这个库启动的时候需要初始化,有几种方式,
比如在LLVMFuzzerTestOneInput
里面,(或者有时候是全局变量 ,直接在全局方位初始化得了)
1 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { |
此外,我们还可以定义一个LLVMFuzzerInitialize
,建议你确实需要argc和argv的时候才使用
1 | extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) { |
泄露
使用AddressSanitizer 或者 LeakSanitizer 构建的程序会在程序关闭后检查内存泄露
但是进程内的模糊测试是不方便的,因为一旦发现导致内存泄露的突变立即去报告这个内存泄露,每次变异后都去检测代价太大
默认情况,libfuzzer在每次变异时会计算malloc和free调用的次数,如果次数不匹配(但是并不是意味着内存泄露),libfuzzer会给到LeakSanitizer过一下,如果确实泄露,才会复制报告,进程退出
如果目标存在大量内存泄露,而且禁止了泄露检查,那么会耗尽内存
开发libfuzzer
在MacOS和Linux上,libfuzzer是LLVM项目的一部分。
其他系统可使用-DLIBFUZZER_ENABLE=YES
来请求编译libfuzzer
编译tests可以使用DLIBFUZZER_ENABLE_TESTS=ON
参考
https://github.com/Dor1s/libfuzzer-workshop
https://docs.google.com/presentation/d/1pbbXRL7HaNSjyCHWgGkbpNotJuiC4O7L_PDZoGqDf5Q/edit#slide=id.p4
https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md
https://chromium.googlesource.com/chromium/src/testing/libfuzzer/+/HEAD/
https://chromium.googlesource.com/chromium/src/testing/libfuzzer/+/HEAD/getting_started.md